﻿/*
 * shaim
 * http://shaim.net
 * Copyright ©2005-2008, shaim dev team
 * Licensed under the Massachusetts Institute of Technology (MIT) License
 * http://www.opensource.org/licenses/mit-license.php
 * 
 */

using System;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Xml;
using LibGreen;

namespace csammisrun.shaim
{
    /// <summary>
    /// Encapsulates one or more <see cref="Contact"/> objects into a meta-contact
    /// </summary>
    public class MetaContact : IComparable, IContactListSerializable, INotifyPropertyChanged,
      IDisposable, IOrderableElement
    {
        #region Static status delay configuration
        private static int statusUpdateDelay = 3000;
        /// <summary>
        /// Gets or sets the delay between Offline and Online in milliseconds
        /// </summary>
        public static int StatusUpdateDelay
        {
            get { return statusUpdateDelay; }
            set { statusUpdateDelay = value; }
        }
        #endregion

        #region Member variables
        private ContactStatus status = ContactStatus.Offline;
        private ContactStatus oldStatus = ContactStatus.Offline;
        private ContactStatus nextStatus = ContactStatus.Offline;
        private readonly ContactCollection contacts;
        private string displayName = "";
        private bool isCollapsed = true;  // Metacontacts are collapsed by default
        private ContactGroup parentGroup;
        private Guid guid;
        private bool isUserDefined;
        private readonly Timer statusUpdateTimer;
        private int preferredOrder = -1;
        private readonly IUIDispatcher dispatcher;
        #endregion

        /// <summary>
        /// Initializes a new MetaContact
        /// </summary>
        internal MetaContact(IUIDispatcher dispatcher)
        {
            this.dispatcher = dispatcher;
            contacts = new ContactCollection(this);
            guid = System.Guid.NewGuid();
            statusUpdateTimer = new Timer(statusUpdateTimer_Elapsed, null, Timeout.Infinite, Timeout.Infinite);
        }

        /// <summary>
        /// Initializes a new MetaContact from an XML node
        /// </summary>
        internal MetaContact(IUIDispatcher dispatcher, XmlNode xmlnode)
            : this(dispatcher)
        {
            // Populate fields from the XmlNode
            DisplayName = xmlnode.Attributes["Name"].Value;
            isUserDefined = String.Compare(xmlnode.Attributes["IsNameUserDefined"].Value, "yes",
              true, CultureInfo.InvariantCulture) == 0;
            guid = new Guid(xmlnode.Attributes["Guid"].Value);
        }

        #region Properties
        /// <summary>
        /// The underlying list of <see cref="Contact"/> objects that are
        /// abstracted by this metacontact
        /// </summary>
        public ContactCollection Contacts
        {
            get { return contacts; }
        }

        /// <summary> 
        /// The name displayed to the shaim user
        /// </summary>
        public string DisplayName
        {
            get { return displayName; }
            internal set
            {
                if (displayName != value)
                {
                    displayName = value;
                    OnPropertyChanged("DisplayName");
                }
            }
        }

        /// <summary>
        /// Gets the <see cref="ContactStatus"/> of the metacontact
        /// </summary>
        public virtual ContactStatus Status
        {
            get { return status; }
            internal set
            {
                if (value != status)
                {
                    status = value;
                    OnPropertyChanged("Status");
                }
            }
        }

        /// <summary>
        /// Gets or sets a value controlling the display of this metacontact
        /// </summary>
        public virtual bool Collapsed
        {
            get { return isCollapsed; }
            set
            {
                if (isCollapsed != value)
                {
                    isCollapsed = value;
                    OnPropertyChanged("Collapsed");
                }
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="ContactGroup"/> to which this metacontact belongs
        /// </summary>
        public ContactGroup Parent
        {
            get { return parentGroup; }
            internal set
            {
                if (parentGroup != value)
                {
                    parentGroup = value;
                    OnPropertyChanged("Parent");
                }
            }
        }

        /// <summary>
        /// Gets the globally unique identifier of this metacontact
        /// </summary>
        public string Guid
        {
            get { return guid.ToString("B"); }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Request an update to the metacontact's display name from a protocol plugin
        /// </summary>
        /// <remarks>This may result in no change if the metacontact has a user-set alias</remarks>
        public void RequestDisplayNameUpdate(IProtocolPlugin caller, string newname)
        {
            if (caller != null && !isUserDefined)
            {
                DisplayName = newname;
            }
        }

        /// <summary>
        /// Sets a permanent alias on the metacontact
        /// </summary>
        /// <exception cref="CantModifyContactException">Thrown when an IProtocolPlugin attemps to call this method</exception>
        public void SetUserDefinedAlias(string newname)
        {
            // It is *important* that no protocol plugin can define this
            foreach (Type t in System.Reflection.Assembly.GetCallingAssembly().GetExportedTypes())
            {
                if (t == typeof(IProtocolPlugin))
                {
                    throw new CantModifyContactException("The permanent metacontact alias cannot be set by a protocol plugin. " +
                      "Use RequestDisplayNameUpdate instead.");
                }
            }

            isUserDefined = true;
            DisplayName = newname;
        }
        #endregion

        #region Internal methods and properties
        /// <summary>
        /// Gets the last known status of the metacontact
        /// </summary>
        internal ContactStatus OldStatus
        {
            get { return oldStatus; }
        }

        /// <summary>
        /// Updates the current status after a change to/from Offline
        /// </summary>
        private void statusUpdateTimer_Elapsed(object state)
        {
            oldStatus = Status;
            Status = nextStatus;
        }

        /// <summary>
        /// Regenerate anything binding to the metacontact itself to respond to child contact property updates
        /// </summary>
        internal void ContactPropertyUpdate(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "IsMobile")
            {
                OnPropertyChanged(e.PropertyName);
            }
        }

        /// <summary>
        /// Regenerate this metacontact's status when a child contact changes its status
        /// </summary>
        internal void ContactStatusUpdate(object sender, StatusUpdateEventArgs e)
        {
            oldStatus = Status;
            ContactStatus newstatus = ContactStatus.Offline;
            foreach (Contact sc in Contacts)
            {
                if ((int)sc.Status > (int)newstatus)
                {
                    newstatus = sc.Status;
                }
            }

            if (oldStatus == ContactStatus.Offline && e.NewStatus != ContactStatus.Offline)
            {
                Status = ContactStatus.NewlyOnline;
                nextStatus = e.NewStatus;
                statusUpdateTimer.Change(StatusUpdateDelay, Timeout.Infinite);
            }
            else if (newstatus == ContactStatus.Offline && e.OldStatus != ContactStatus.Offline)
            {
                Status = ContactStatus.NewlyOffline;
                nextStatus = ContactStatus.Offline;
                statusUpdateTimer.Change(StatusUpdateDelay, Timeout.Infinite);
            }
            else
            {
                // Save the status for restoration after the status update timer elapses
                nextStatus = newstatus;
                if (Status != ContactStatus.NewlyOffline && Status != ContactStatus.NewlyOnline)
                {
                    Status = nextStatus;
                }
            }
        }
        #endregion

        #region IComparable Members
        /// <summary>
        /// Compares the current instance with another object of the same type.
        /// </summary>
        /// <param name="obj">An object to compare with this instance.</param>
        /// <returns>A 32-bit signed integer that indicates the relative order of the objects being compared.</returns>
        public int CompareTo(object obj)
        {
            MetaContact other = obj as MetaContact;
            if (other != null)
            {
                return Guid.CompareTo(other.Guid);
            }

            return -1;
        }

        /// <summary>
        /// Determines whether the specified Object is equal to the current Object.
        /// </summary>
        /// <param name="obj">The Object to compare with the current Object.</param>
        /// <returns>true if the specified Object is equal to the current Object; otherwise, false.</returns>
        public override bool Equals(object obj)
        {
            return (CompareTo(obj) == 0);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>A hash code for the current Object.</returns>
        public override int GetHashCode()
        {
            return Guid.GetHashCode();
        }
        #endregion

        #region IContactListSerializable Members
        /// <summary>
        /// Gets the XPath expression that will locate the given node relative to its parent
        /// </summary>
        public string XPath
        {
            get { return String.Format(CultureInfo.InvariantCulture, "MetaContact[@Guid='{0}']", Guid); }
        }
        /// <summary>
        /// Sets item-specific attributes on an XmlNode
        /// </summary>
        public void SetXmlNodeAttributes(XmlNode node)
        {
            XmlAttribute attrib;

            attrib = node.OwnerDocument.CreateAttribute("Name");
            attrib.Value = DisplayName;
            node.Attributes.SetNamedItem(attrib);

            attrib = node.OwnerDocument.CreateAttribute("IsNameUserDefined");
            attrib.Value = (isUserDefined) ? "Yes" : "No";
            node.Attributes.SetNamedItem(attrib);

            attrib = node.OwnerDocument.CreateAttribute("Guid");
            attrib.Value = Guid;
            node.Attributes.SetNamedItem(attrib);
        }
        #endregion

        #region INotifyPropertyChanged Members
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        private delegate void OnPropertyChangedDispatcher(string propertyName);

        private void OnPropertyChanged(string propertyName)
        {
            if (dispatcher == null || dispatcher.CheckAccess())
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            else
            {
                dispatcher.Dispatch(new OnPropertyChangedDispatcher(OnPropertyChanged), propertyName);
            }
        }

        #endregion

        /// <summary>
        /// Returns a string representation of a metacontact
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return String.Format(CultureInfo.InvariantCulture, "Metacontact {0}", DisplayName);
        }

        #region IDisposable Members
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <param name="disposing">A value indicating whether or not it's really disposing</param>
        protected virtual void Dispose(bool disposing)
        {
            statusUpdateTimer.Dispose();
        }

        #endregion

        #region IOrderableElement Members

        /// <summary>
        /// Gets the preferred order of the list element
        /// </summary>
        public int PreferredOrder
        {
            get { return preferredOrder; }
            internal set { preferredOrder = value; }
        }

        #endregion
    }
}
